package com.ztesoft.book.admin.config.shiro;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.ztesoft.book.admin.config.shiro.jwt.JwtToken;
import com.ztesoft.book.common.constant.AuthorizationThrow;
import com.ztesoft.book.common.constant.Constant;
import com.ztesoft.book.common.utils.encrypt.AesEncryptSm4Util;
import com.ztesoft.book.common.utils.exception.ExUtil;
import com.ztesoft.book.common.utils.exception.ServiceException;
import com.ztesoft.book.common.utils.jwt.JwtUtil;
import com.ztesoft.book.common.utils.redis.RedisUtils;
import com.ztesoft.book.core.sys.sysmenu.dto.SysMenuDTO;
import com.ztesoft.book.core.sys.sysmenu.mapper.SysMenuMapper;
import com.ztesoft.book.core.sys.sysrole.dto.SysRoleDTO;
import com.ztesoft.book.core.sys.sysrole.mapper.SysRoleMapper;
import com.ztesoft.book.core.sys.sysuser.dto.SysUserDTO;
import com.ztesoft.book.core.sys.sysuser.mapper.SysUserMapper;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @Description Shiro自定义认证
 * @Author YangFan
 * @Date 2021-06-15 14:45
 */
@Component
public class MyShiroRealm extends AuthorizingRealm {


    private final SysUserMapper userMapper;
    private final SysRoleMapper roleMapper;
    private final SysMenuMapper menuMapper;
    private static String redisTokenRefreshTime;

    @Autowired
    @Lazy
    public MyShiroRealm(SysUserMapper userMapper, SysRoleMapper roleMapper, SysMenuMapper menuMapper) {
        this.userMapper = userMapper;
        this.roleMapper = roleMapper;
        this.menuMapper = menuMapper;
    }

    @Value("${book.time.redis-token-refresh}")
    public void setRedisTokenRefreshTime(String redisTokenRefreshTime) {
        setRedisTokenRefreshTimeValue(redisTokenRefreshTime);
    }

    public static void setRedisTokenRefreshTimeValue(String redisTokenRefreshTime) {
        MyShiroRealm.redisTokenRefreshTime = redisTokenRefreshTime;
    }

    /**
     * 方法功能描述:
     * < 大坑，必须重写此方法，不然Shiro会报错 >
     *
     * @param authenticationToken 【参数 1 注释说明】
     * @return boolean                        【返回值 注释说明】
     * @author YangFan
     * @date 2021-06-22 16:51
     */
    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof JwtToken;
    }


    /**
     * 方法功能描述:
     * < 只有当需要检测用户权限的时候才会调用此方法，例如checkRole,checkPermission之类的 >
     *
     * @param principalCollection 【参数 1 注释说明】
     * @return AuthorizationInfo            【返回值 注释说明】
     * @author YangFan
     * @date 2021-06-22 16:51
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        String username = JwtUtil.getClaim(principalCollection.toString(), Constant.USERNAME);
        SysUserDTO user = new SysUserDTO();
        user.setUsername(AesEncryptSm4Util.encrypt(username));
        // 查询用户角色
        List<SysRoleDTO> roles = roleMapper.findRoleByUser(user);
        for (SysRoleDTO role : roles) {
            if (role != null) {
                if (StringUtils.isNotBlank(role.getRoleCode())) {
                    // 添加角色
                    simpleAuthorizationInfo.addRole(role.getRoleCode() + "");
                }
                // 根据用户角色查询权限
                List<SysMenuDTO> menus = menuMapper.findMenuByRole(role);
                for (SysMenuDTO menu : menus) {
                    if (menu != null) {
                        if (StringUtils.isNotBlank(menu.getPermission())) {
                            // 添加权限
                            simpleAuthorizationInfo.addStringPermission(menu.getPermission());
                        }
                    }
                }
            }
        }
        return simpleAuthorizationInfo;
    }


    /**
     * 方法功能描述:
     * < 默认使用此方法进行用户名正确与否验证，错误抛出异常即可 >
     *
     * @param authenticationToken 【参数 1 注释说明】
     * @return AuthenticationInfo            【返回值 注释说明】
     * @author YangFan
     * @date 2021-06-22 16:52
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authenticationToken) throws AuthenticationException {
        String token = (String) authenticationToken.getCredentials();
        // 解密获得username，用于和数据库进行对比
        String username = null;
        try {
            username = JwtUtil.getClaim(token, Constant.USERNAME);
        }
        catch (Exception e) {
            ExUtil.throwEx(AuthorizationThrow.JWT_TOKEN_EXPLAIN_ERROR, AuthenticationException.class);
        }
        // 帐号为空
        if (StringUtils.isBlank(username)) {
            throw new AuthenticationException("Token中帐号为空！");
        }
        // 查询用户是否存在，先对用户名进行EXT加密再与数据库比对
        QueryWrapper<SysUserDTO> wrapper = new QueryWrapper<>();
        wrapper.eq("username", AesEncryptSm4Util.encrypt(username));
        SysUserDTO user = userMapper.selectOne(wrapper);
        if (user == null) {
            ExUtil.throwEx("该帐号不存在！", AuthenticationException.class);
        }
        // 开始认证，要AccessToken认证通过，且Redis中存在RedisToken，且两个Token时间戳一致
        try {
            if (JwtUtil.verify(token) && RedisUtils.exists(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username)) {
                // RedisToken剩余时间检查，判断是否需要续期（这里直接判断，不会存在-1和-2情况）
                Long extime = RedisUtils.getExpire(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username);
                if (extime < Long.parseLong(redisTokenRefreshTime)) {
                    ExUtil.throwEx(AuthorizationThrow.REDIS_TOKEN_NEED_REFRESH);
                }

                // 获取RefreshToken的时间戳
                Map<String, Object> redisToken = RedisUtils.get(Constant.PREFIX_SHIRO_REFRESH_TOKEN + username,
                        HashMap.class);
                if (redisToken != null) {
                    String currentTimeMillisRedis = redisToken.get("currentTimeMillis")
                            .toString();
                    // 获取AccessToken时间戳，与RefreshToken的时间戳对比
                    boolean b = true;

                    // 这里校验时间戳则可以保证单点登录，不校验则同账号可以多处同时登录
                    if (!JwtUtil.getClaim(token, Constant.CURRENT_TIME_MILLIS)
                            .equals(currentTimeMillisRedis)) {
                        b = false;
                    }

                    if (b) {
                        return new SimpleAuthenticationInfo(token, token, "userRealm");
                    }
                }
            }
        }
        catch (ServiceException e) {
            // 如还有其他自定义异常，则在这里通过message判断再抛出
            ExUtil.throwEx(AuthorizationThrow.REDIS_TOKEN_NEED_REFRESH, AuthenticationException.class);

        }
        catch (Exception e) {
            ExUtil.throwEx(AuthorizationThrow.JWT_TOKEN_EXPIRE, AuthenticationException.class);
        }
        ExUtil.throwEx("RedisToken已过期！", AuthenticationException.class);
        return null;
    }


}
